#region License

// Another free, open solution by someone who gets it.
// 
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#endregion

using System;
using System.Diagnostics;
using System.Linq.Expressions;

using Patterns.Validation.Framework;

namespace Patterns.Validation.Advanced
{
	/// <summary>
	/// 	Contains a number of methods to assert that a reference type object is in the expected state.
	/// </summary>
	[DebuggerNonUserCode]
	public abstract class ReferenceTypeValidator<TSubject, TValidator> where TValidator : ReferenceTypeValidator<TSubject, TValidator>
	{
		/// <summary>
		/// 	Gets the object which value is being asserted.
		/// </summary>
		public TSubject Subject{ get; protected set; }

		/// <summary>
		/// 	Validates that the object is of the specified type <typeparamref name = "T" />.
		/// </summary>
		/// <typeparam name = "T">The expected type of the object.</typeparam>
		/// <param name = "reason">
		/// 	A formatted phrase as is supported by <see cref = "string.Format(string,object[])" /> explaining why the assertion 
		/// 	is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
		/// </param>
		/// <param name = "reasonArgs">
		/// 	Zero or more objects to format using the placeholders in <see cref = "reason" />.
		/// </param>
		public AndConstraint<TValidator> BeOfType<T>(string reason = null, params object[] reasonArgs)
		{
			return BeOfType(typeof (T), reason, reasonArgs);
		}

		/// <summary>
		/// 	Validates that the object is of the specified type.
		/// </summary>
		/// <param name = "expected">The expected type.</param>
		/// <param name = "reason">
		/// 	A formatted phrase as is supported by <see cref = "string.Format(string,object[])" /> explaining why the assertion 
		/// 	is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.</param>
		/// <param name = "reasonArgs">
		/// 	Zero or more objects to format using the placeholders in <see cref = "reason" />.</param>
		public AndConstraint<TValidator> BeOfType(Type expected, string reason = null, params object[] reasonArgs)
		{
			Execute.Verification
				.ForCondition(expected == Subject.GetType())
				.BecauseOf(reason, reasonArgs)
				.FailWith("Expected type {0}{reason}, but found {1}.", expected, Subject.GetType());

			return new AndConstraint<TValidator>((TValidator) this);
		}

		/// <summary>
		/// 	Validates that the object is assignable to a variable of the specified type.
		/// </summary>
		/// <param name = "expected">The expected type.</param>
		/// <param name = "reason">The reason why the object should be assignable to the type.</param>
		/// <param name = "reasonArgs">The parameters used when formatting the <paramref name = "reason" />.</param>
		/// <returns>
		/// 	An <see cref = "AndConstraint{T}" /> which can be used to chain Validator.
		/// </returns>
		public AndConstraint<TValidator> BeAssignableTo(Type expected, string reason = null, params object[] reasonArgs)
		{
			Execute.Verification
				.ForCondition(expected.IsAssignableFrom(Subject.GetType()))
				.BecauseOf(reason, reasonArgs)
				.FailWith("Expected to be assignable to {0}{reason}, but {1} does not implement {0}", expected, Subject.GetType());

			return new AndConstraint<TValidator>((TValidator) this);
		}

		/// <summary>
		/// 	Validates that the object is assignable to a variable of type <typeparamref name = "T" />.
		/// </summary>
		/// <typeparam name = "T">The type to which the object should be assignable.</typeparam>
		/// <param name = "reason">The reason why the object should be assignable to the type.</param>
		/// <param name = "reasonArgs">The parameters used when formatting the <paramref name = "reason" />.</param>
		/// <returns>An <see cref = "AndConstraint{T}" /> which can be used to chain Validator.</returns>
		public AndConstraint<TValidator> BeAssignableTo<T>(string reason = null, params object[] reasonArgs)
		{
			return BeAssignableTo(typeof (T), reason, reasonArgs);
		}

		/// <summary>
		/// 	Validates that the <paramref name = "predicate" /> is statisfied.
		/// </summary>
		/// <param name = "predicate">The predicate which must be satisfied by the <typeparamref name = "TSubject" />.</param>
		/// <returns>An <see cref = "AndConstraint{T}" /> which can be used to chain Validator.</returns>
		public AndConstraint<ReferenceTypeValidator<TSubject, TValidator>> Match(Expression<Func<TSubject, bool>> predicate)
		{
			return Match<TSubject>(predicate, String.Empty);
		}

		/// <summary>
		/// 	Validates that the <paramref name = "predicate" /> is satisfied.
		/// </summary>
		/// <param name = "predicate">The predicate which must be statisfied by the <typeparamref name = "TSubject" />.</param>
		/// <param name = "reason">The reason why the predicate should be satisfied.</param>
		/// <param name = "reasonArgs">The parameters used when formatting the <paramref name = "reason" />.</param>
		/// <returns>An <see cref = "AndConstraint{T}" /> which can be used to chain Validator.</returns>
		public AndConstraint<ReferenceTypeValidator<TSubject, TValidator>> Match(Expression<Func<TSubject, bool>> predicate, string reason,
		                                                                          params object[] reasonArgs)
		{
			return Match<TSubject>(predicate, reason, reasonArgs);
		}

		/// <summary>
		/// 	Validates that the <paramref name = "predicate" /> is satisfied.
		/// </summary>
		/// <param name = "predicate">The predicate which must be statisfied by the <typeparamref name = "TSubject" />.</param>
		/// <returns>An <see cref = "AndConstraint{T}" /> which can be used to chain Validator.</returns>
		public AndConstraint<ReferenceTypeValidator<TSubject, TValidator>> Match<T>(Expression<Func<T, bool>> predicate)
			where T : TSubject
		{
			return Match(predicate, string.Empty);
		}

		/// <summary>
		/// 	Validates that the <paramref name = "predicate" /> is satisfied.
		/// </summary>
		/// <param name = "predicate">The predicate which must be statisfied by the <typeparamref name = "TSubject" />.</param>
		/// <param name = "reason">The reason why the predicate should be satisfied.</param>
		/// <param name = "reasonArgs">The parameters used when formatting the <paramref name = "reason" />.</param>
		/// <returns>An <see cref = "AndConstraint{T}" /> which can be used to chain Validator.</returns>
		public AndConstraint<ReferenceTypeValidator<TSubject, TValidator>> Match<T>(Expression<Func<T, bool>> predicate, string reason,
		                                                                             params object[] reasonArgs)
			where T : TSubject
		{
			if (predicate == null)
				throw new NullReferenceException("Cannot match an object against a <null> predicate.");

			Execute.Verification
				.ForCondition(predicate.Compile()((T) Subject))
				.BecauseOf(reason, reasonArgs)
				.FailWith("Expected {0} to match {1}{reason}.", Subject, predicate.Body);

			return new AndConstraint<ReferenceTypeValidator<TSubject, TValidator>>(this);
		}
	}
}